--[[
Name: unitDoodles
Author: Sonora (The Dragonflight, US Kirin Tor)
Website & SVN: http://code.google.com/p/browniehelper/source/browse/#svn/trunk/unitDoodles
License: GNU General Public License v3
]]

--Utilities
local function deepcopy(object)
    local lookup_table = {}
    local function _copy(object)
        if type(object) ~= "table" then
            return object
        elseif lookup_table[object] then
            return lookup_table[object]
        end
        local new_table = {}
        lookup_table[object] = new_table
        for index, value in pairs(object) do
            new_table[_copy(index)] = _copy(value)
        end
        return setmetatable(new_table, _copy(getmetatable(object)))
    end
    return _copy(object)
end

--Application namespace table with some constants
unitDoodles = {

    --Constants
    FORWARD = true,
    REVERSE = false,
    
    RIGHT = 0,
    LEFT = 1,
    TOP = 3,
    
    BUFFS = "Buffs",
    DEBUFFS = "Debuffs",
    
    VERTICAL = 'VERTICAL',
    HORIZONTAL = 'HORIZONTAL',
    
    eval = function(evalString)
        return assert(loadstring("return unitDoodles."..evalString))()
    end,
}

--[[
For now expose the entire global table to other modules, and eval.  It is 
important to note, however, that we currently eval strings which are globally
exposed (through unitDoodles.config)]]--
setmetatable(unitDoodles, {__index = _G})

--Event handling & plumbing
unitDoodles.eventFrame = CreateFrame("Frame","unitDoodlesFrame")
unitDoodles.eventHandlers = {

    ADDON_LOADED = function(addonName)
        if addonName == 'unitDoodles' then            
    
            --Fire up the reactor!
            unitDoodles.reactor:start()
        
        end
    end,
    
    PLAYER_ENTERING_WORLD = function()
    
        --Fire configuration and setup
        unitDoodles.buildDoodles()
    
        --Fire an aura update to get everything into a sane state
        unitDoodles.eventHandlers.UNIT_AURA()
    
    end,
    
    UNIT_AURA = function(unitID)
        --Call all unitID associated aura listeners
    
        
        if unitID == nil then
            --Ambiguous unitID, update everything (inefficient implementation)
            for i, doodle in ipairs(unitDoodles.auraListeners or {}) do
                doodle:handler()
            end
        else
            for i, doodle in ipairs(unitDoodles.auraListeners or {}) do
                if doodle.unitID == unitID or UnitIsUnit(doodle.unitID, unitID) then
                    doodle:handler()
                end
            end
        end
    
        --[[
        for i, doodle in ipairs(unitDoodles.auraListeners or {}) do
            doodle:handler()
        end
        ]]--
    end,

    PLAYER_TARGET_CHANGED = function()
        unitDoodles.eventHandlers.UNIT_AURA('target')
    end,
    
    RAID_ROSTER_UPDATE = function()
        unitDoodles.updateGroupUnits(true)
    end,
    
    PARTY_MEMBERS_CHANGED = function()
        unitDoodles.updateGroupUnits(true)
    end,
    
}

unitDoodles.eventFrame:SetScript("OnEvent", 
    function(self, event, ...)
        local handler = unitDoodles.eventHandlers[event]
        if handler then handler(...) end
    end
)
for k,v in pairs(unitDoodles.eventHandlers) do
    unitDoodles.eventFrame:RegisterEvent(k)
end

--Event listener registration
function unitDoodles.addAuraListener(doodle)
    table.insert(unitDoodles.auraListeners, doodle)
end

--Doodle builder (here is where doodle tables are interpreted)
function unitDoodles.clearDoodles()
    --[[
    A little non-intuitive, but here's where we setup all structures that 
    reference doodles, and are sure to de-reference them all on clears
    ]]--
    
    --TODO:  there appear to be memory leaks here still
    
    --Clear out the reactor pool
    unitDoodles.reactor:clear()
    
    --Traverse doodle structures and hide and de-reference them from parents
    for k,doodles in pairs(unitDoodles.units or {}) do
        for i, doodle in ipairs(doodles) do
            doodle:Hide()
            doodle:SetParent(nil)
        end
    end
    
    --De-reference doodles from other internal structures
    unitDoodles.auraListeners = {}

    unitDoodles.units = {
        player = {},
        target = {},
        targettarget = {},
        pet = {},
        focus = {},
        focustarget = {},
    }

end

--[[
Doodle inheritance engine
]]--
local function multipleInheritance(inheritanceStack)
    return function(t, k)
            for _, parent in ipairs(inheritanceStack) do
                if type(parent) == 'function' then 
                    return parent(t, k)
                elseif parent[k] ~= nil then 
                    return parent[k] 
                end
            end
        end
end

function unitDoodles.setupInheritance(obj)
    if obj.inherits then
        local previousMeta = getmetatable(obj)
        if previousMeta and previousMeta.__index then
            local newMeta = {unpack(getmetatable(obj))}
            newMeta['__index'] = multipleInheritance({obj.inherits, previousMeta['__index']})
            setmetatable(obj, newMeta)
        else
            local newMeta = {unpack(getmetatable(obj) or {})}
            newMeta['__index'] = obj.inherits
            setmetatable(obj, newMeta)
        end
    end
end

function unitDoodles.initializeDoodle(doodleClass, doodleInstance, unitID, parentFrame)
    
    local config, units = unitDoodles.config, unitDoodles.units
    
    --Setup inheritance for new instance (also the UI element)
    doodleInstance.inherits = doodleClass
    unitDoodles.setupInheritance(doodleInstance)
    
    table.insert(units[unitID], doodleInstance)
    unitDoodles.positionDoodle(doodleInstance, parentFrame, #units[unitID], units[unitID])
    
    --Little wierd to have this here; if we add more magic later, split out
    if doodleInstance.mouseOver then
        doodleInstance:EnableMouse(true)
        doodleInstance:SetScript("OnEnter", function() doodleInstance:mouseOver() end)
        doodleInstance:SetScript("OnLeave", function() if GameTooltip:IsShown() then GameTooltip:Hide() end end)
    end
    
    --[[
    If this is a party frame doodle and the player is shown as the 
    first group member we have to adjust the associated unitID
    ]]--
    if  string.sub(unitID, 1, 5) == 'party' and 
        config.frameSets[config.currentFrameSet].playerInGroupFrames then

        local partyNum = tonumber(string.sub(unitID, 6))
        if partyNum == 1 then
            doodleInstance.unitID = 'player'
        else
            doodleInstance.unitID = 'party'..tostring(partyNum-1)
        end
    end
    
    --If this frameset offers a UnitID override call
    if parentFrame and config.frameSets[config.currentFrameSet].getUnitID ~= nil then
        local newUnitID = config.frameSets[config.currentFrameSet].getUnitID(parentFrame)
        if newUnitID then doodleInstance.unitID = newUnitID end
    end
    
    --Return doodle instance
    return doodleInstance

end

function unitDoodles.setPoints(doodle, anchorFrame, setPoint, noClear)

    --If we haven't been told not to clear existing points, do so
    if not noClear then doodle:ClearAllPoints() end
    
    --If setPoints is a list of setPoints, call us with each
    if type(setPoint[1]) == 'table' then
        for i,isetPoint in ipairs(setPoint) do
            unitDoodles.setPoints(doodle, anchorFrame, isetPoint, true)
        end 
    
    --Otherwise, set the position
    else
        if doodle.height then doodle:SetHeight(doodle.height) end
        if doodle.width then doodle:SetWidth(doodle.width) end
        
        local position = {unpack(setPoint)}
        table.insert(position, 2, anchorFrame)
        doodle:SetPoint(unpack(position))
    end

end

function unitDoodles.positionDoodle(doodle, anchorFrame, doodleIndex, unitDoodleSet)

    --The doodle explicitly sets a point
    if doodle.setPoint then
        unitDoodles.setPoints(doodle, anchorFrame, doodle.setPoint)
    
    --Doodle advances position from a previous
    elseif doodle.advancePosition then
    
        --Find the closest doodle, and set our position relative to it
        local referenceDoodle = unitDoodleSet[doodleIndex-1]
        unitDoodles.setPoints(doodle, referenceDoodle, doodle.advancePosition)
        
        --TODO:  error checking for a series of doodles with no starting set point
    
    --Doodle advances position based on the closest shown doodle
    elseif doodle.stackPosition then
        --[[
        Find the closest doodle of this type that either has a SetPoint or is
        associated with a shown texture]]
        for backIdx = 1,(doodleIndex-1) do

            local referenceDoodle = unitDoodleSet[doodleIndex-backIdx]
    
            if referenceDoodle:IsShown() then
                unitDoodles.setPoints(doodle, referenceDoodle, doodle.stackPosition)
                break
            elseif referenceDoodle.setPoint then
                unitDoodles.setPoints(doodle, anchorFrame, referenceDoodle.setPoint)
                break
            end

        end
    
        --TODO: error handling for invalid stack structures
    
    end
    
    --TODO: error handling for un-positionable doodles
end

function unitDoodles.getFrame(unitID)

    local currentFrameSet = unitDoodles.config.frameSets[unitDoodles.config.currentFrameSet]

    if string.sub(unitID, 1, 5) == 'party' then
    
        if currentFrameSet.party then
            local partyIndex = string.sub(unitID, 6)
            local frameName = currentFrameSet['party']
            return _G[string.gsub(frameName, "$p", partyIndex)]
        
        elseif currentFrameSet.getFrame then
            return currentFrameSet:getFrame(unitID)
        end
    
    elseif string.sub(unitID, 1, 4) == 'raid' then
    
        if currentFrameSet.raid then
            local raidIndex = string.sub(unitID, 5)
            local groupIndex = tostring(math.ceil(tonumber(raidIndex)/5))
            local partyIndex = tostring(tonumber(raidIndex) - ((tonumber(groupIndex)-1)*5))
        
            local frameName = currentFrameSet['raid']
        
            frameName = string.gsub(frameName, "$p", partyIndex)
            frameName = string.gsub(frameName, "$r", raidIndex)
            frameName = string.gsub(frameName, "$g", groupIndex)

            return _G[frameName]
        
        elseif currentFrameSet.getFrame then
            return currentFrameSet:getFrame(unitID)
        end
    
    else
        if currentFrameSet[unitID] then
            local frameName = currentFrameSet[unitID]
            return _G[frameName]
        elseif currentFrameSet.getFrame then
            return currentFrameSet:getFrame(unitID)
        end
    end
end

function unitDoodles.updateDoodlePositions(unitID)
    if unitID == nil then
        for unitID,_ in pairs(unitDoodles.units) do
            unitDoodles.updateDoodlePositions(unitID)
        end
    else
       for doodleIndex, doodle in ipairs(unitDoodles.units[unitID]) do
            unitDoodles.positionDoodle(doodle, unitDoodles.getFrame(unitID), doodleIndex, unitDoodles.units[unitID])
       end     
    end
end

function unitDoodles.buildUnitDoodles(unitID)

    local doodleSet = unitDoodles.config.doodles[unitID]

    local doodleOffset = 0
    for doodleIndex, doodleClass in ipairs(doodleSet) do

        local parentFrame = unitDoodles.getFrame(unitID)
    
        --Build the doodle instance from the class table
        local doodleInstance = doodleClass:builder(unitID, parentFrame)
    
        if doodleInstance.returnsMany == true then
            for i, d in ipairs(doodleInstance) do
                doodleOffset = doodleOffset + i --TODO are we zero indexed?
                unitDoodles.initializeDoodle(doodleClass, d, unitID, parentFrame)
            end
        else
            unitDoodles.initializeDoodle(doodleClass, doodleInstance, unitID, parentFrame)
        end
    
    end
    
    unitDoodles.updateDoodlePositions(unitID)
    
end

function unitDoodles.updateGroupUnits(buildNew)
    --Update all possible party frames with fresh doodles
    
    local config, units = unitDoodles.config, unitDoodles.units
    local newDoodles = {}
    
    --Make all possible party doodles
    for i = 1,5 do
        local partyFrame = unitDoodles.getFrame('party'..tostring(i))
        if partyFrame and not units['party'..tostring(i)] then
            units['party'..tostring(i)] = {}
            config.doodles['party'..tostring(i)] = config.doodles.party or {}
            if buildNew then unitDoodles.buildUnitDoodles('party'..tostring(i)) end
        end
    end
    
    --Make all possible raid doodles
    for i = 1,40 do
        local raidFrame = unitDoodles.getFrame('raid'..tostring(i))
        if raidFrame and not units['raid'..tostring(i)] then
            units['raid'..tostring(i)] = {}
            config.doodles['raid'..tostring(i)] = config.doodles.raid or {}
            if buildNew then unitDoodles.buildUnitDoodles('raid'..tostring(i)) end
        end
    end
    
end


function unitDoodles.configSetup()
    
    local config, eval, setupInheritance = unitDoodles.config, unitDoodles.eval, unitDoodles.setupInheritance
    
    --Setup a fresh .doodles structure
    config.doodles = {}
    
    --If this is the first time we've been run, save the un-altered doodle state for future restores
    if unitDoodles.cleanDoodles == nil then
        unitDoodles.cleanDoodles = deepcopy(unitDoodles.doodles)
    else
    --Otherwise, restore before setting up defaults
        unitDoodles.doodles = deepcopy(unitDoodles.cleanDoodles)
    end
    
    --Apply default config entries to all listed doodles
    for doodleReference, defaultConfig in pairs(config.doodleDefaults) do
        doodle = unitDoodles.eval(doodleReference)
        for k,v in pairs(defaultConfig) do
            if doodle[k] == nil then doodle[k]=v end
        end
    end
    
    --For each doodleSet we are instructed to load
    for _, doodleSetName in ipairs(config.loadDoodleSets) do
        local doodleSet = config.doodleSets[doodleSetName]
        --assert(doodleSet, "Invalid doodleSetName "..doodleSetName)
        if doodleSet == nil then
            print("Unable to load doodle set named: "..doodleSetName) 
    
        else
            --For each unit-specific doodle set in the lest
            for unitName, doodleList in pairs(doodleSet) do
        
                config.doodles[unitName] = config.doodles[unitName] or {}
        
                --For each doodle in the set
                for _, doodle in ipairs(doodleList) do
            
                    --Add this doodle to config.doodles (consumed by .buildDoodles)
                    table.insert(config.doodles[unitName], doodle)
                    
                    --Setup doodle inheritance
                    if doodle.doodleType then doodle.inherits = eval(doodle.doodleType) end
                    setupInheritance(doodle)
            
                end
            end
        end
    end
end

--[[
Call unitDoodles.buildDoodles() to initialize the configuration and whenever 
config structure changes need to be re-propagated before drawing doodles.

This function builds the .doodles structure based on the current state of the
.doodleSets and .loadDoodleSets.  We also handle setup of .doodles inheritance. 
Finally, this function also handles application of .doodleDefaults.
]]--
function unitDoodles.buildDoodles()
    
    local config, units = unitDoodles.config, unitDoodles.units

    --Update the configuration
    unitDoodles.configSetup()
    
    --Clean wipe and rebuild of all managed doodles
    unitDoodles.clearDoodles()
    
    --Apply doodle macros across frames
    for macroName, targetUnits in pairs(config.unitMacros) do
        if config.doodles[macroName] ~= nil then
            for _, targetUnit in ipairs(targetUnits) do
                if config.doodles[targetUnit] == nil then config.doodles[targetUnit] = {} end
                for i, doodle in ipairs(config.doodles[macroName]) do
                    table.insert(config.doodles[targetUnit], doodle)
                end
            end
        end
    end
    
    --After macros have been applied, update the unit list with group units
    unitDoodles.updateGroupUnits()
    
    --Discover doodles, and build them all
    for unitID, doodleSet in pairs(config.doodles) do
        --if this isn't a macro entry
        if config.unitMacros[unitID] == nil and unitID ~= 'party' and unitID ~= 'raid' then 
            unitDoodles.buildUnitDoodles(unitID)
        end
    end
    
end

--Aura querying functions
local function getAuraIndex(unit, aura)

    --TODO this is ridiculous, there must be a better way
    for i=1,50 do
        if aura == UnitBuff(unit, i) then
            return i, "HELPFUL"
        elseif aura == UnitDebuff(unit, i) then
            return i, "HARMFUL"
        end
    end
    
end

local function __queryAura(unit, aura, filter, queryCall)
    --[[
    A note about querying auras and this implementaion:

    The current documentation on WoWwiki states that:

    UnitBuff(unit, index or name [, castable])
    UnitDebuff(unitID, index or name [, removable])

    Are aliases for:

    UnitAura("unit", index or ["name", "rank"][, "filter"])

    With filter = "HELPFUL" and "HARMFUL" respectively.  However, this does not 
    actually appear to be the case.  It is likely that this stated behavior was 
    intended, but, as in so many things, the underlying implementation is 
    brittle.

    Althought both types of calls behave similarly when referncing Auras by 
    index, UnitAura is inconsistant in its response to calls using spell names.  
    I have observed many instances where:

    UnitDebuff('target', 'Soothe Animal') returns debufInfo
    UnitAura('target', 'Soothe Animal', 'HARMFUL') returns nil

    I have had a similar experience with UnitBuff.
    ]]--


    local auraInfo = {queryCall(unit, aura, filter)}
    
    if auraInfo[1] then
        local auraIndex, filter = getAuraIndex(unit, aura)
        return {name = auraInfo[1], 
                rank = auraInfo[2], 
                icon = auraInfo[3], 
                count = auraInfo[4], 
                debuffType = auraInfo[5], 
                duration = auraInfo[6], 
                expirationTime = auraInfo[7], 
                unitCaster = auraInfo[8],
                isMine = auraInfo[8]=='player',
                isStealable = auraInfo[9],
                auraIndex = auraIndex,
                filter = filter}
    end
    
end
        
function unitDoodles.queryAura(unit, aura, filter)
    --[[
    aura is the string name of a spell
    filter is 'HELPFUL' or 'HARMFUL'
    ]]--

    for i,queryCall in ipairs({UnitBuff, UnitDebuff}) do
        local result = __queryAura(unit, aura, filter, queryCall)
        if result then return result end
    end
    
end

